﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Antlr.Runtime.Tree;
using JetBrains.Annotations;
using Kaleida.ServiceMonitor.Model.Runtime;
using Kaleida.ServiceMonitor.Model.StructuredSource;

namespace Kaleida.ServiceMonitor.Model.Parsing
{
    public static class ScriptParser
    {
        [CanBeNull]
        private static ScriptCompiler compiler;
        
        [CanBeNull]
        private static ReflectionOperationFactory commandFactory;

        public static void Initialise(string searchDirectoryPath)
        {
            if(!Directory.Exists(searchDirectoryPath))
                throw new InvalidOperationException(string.Format("The required directory '{0}' does not exist", searchDirectoryPath));

            var dllPaths = Directory.GetFiles(searchDirectoryPath, "*.dll");
            var assemblies = dllPaths.Select(LoadFile);
            commandFactory = new ReflectionOperationFactory(assemblies);
			Initialise(commandFactory, commandFactory);
        }

    	internal static void Initialise(IPreparedRequestFactory preparedRequestFactory, IResponseHandlerFactory responseHandlerFactory)
    	{
    		compiler = new ScriptCompiler(preparedRequestFactory, responseHandlerFactory);
    	}

    	public static RuntimeEnvironment GetRuntimeEnvironment()
        {
            if(commandFactory == null) throw new InvalidOperationException("Must first call ScriptParser.Initialise");
            return commandFactory.GetRuntimeEnvironment();
        }

        private static Assembly LoadFile(string path)
        {
            try
            {
                return Assembly.LoadFile(path);
            }
            catch (NotSupportedException)
            {
                throw new InvalidOperationException(string.Format("Could not load '{0}'. Please ensure that all DLL files in this directory are unblocked. In Windows Explorer right-mouse on the file, click 'Properties', then the 'Unblock' button", path));
            }
        }

        internal static ScriptDefinition Parse(this string scriptSource)
        {
            try
            {
                var tree = scriptSource.GetSyntaxTree();
                return BuildScript(tree);
            }
            catch (Exception e)
            {
                throw new InvalidOperationException(string.Format("An error occurred whilst parsing '{0}': {1}", scriptSource, e.Message));
            }
        }

        public static CompiledScript ParseAndBuild(this string text, string defaultName)
        {
            if (compiler == null) throw new InvalidOperationException("Must first call ScriptParser.Initialise");

            var scriptDefinition = text.Parse();
            var executableScript = compiler.Build(scriptDefinition, defaultName);
            return executableScript;
        }

        private static ScriptDefinition BuildScript(ITree tree)
        {
            var nodes = tree.GetChildren();
            var ungroupedDirectives = nodes.Where(i => i.Type == ScriptGrammarLexer.DIRECTIVE).Select(BuildDirective).ToList();
            var ungroupedOperations = nodes.Where(i => i.Type == ScriptGrammarLexer.COMMAND).Select(BuildCommandWithHandler).ToList();
            var ungroupedComments = nodes.Where(i => i.Type == ScriptGrammarLexer.COMMENT).Select(BuildComment).ToList();

            var ungrouped = new ScriptDefinition(ungroupedDirectives, ungroupedOperations, ungroupedComments);

            var groups = nodes.Where(i => i.Type == ScriptGrammarLexer.GROUP).Select(BuildScript).ToList();

            if (groups.Any())
            {
                if(ungrouped.Directives.Any() || ungrouped.Operations.Any() || ungrouped.Comments.Any())
                {
                    groups.Add(ungrouped);
                }

                return new ScriptDefinition(groups);
            }

            return ungrouped;
        }

        private static DirectiveDefinition BuildDirective(ITree node)
        {
            ValidateNodeType(node, ScriptGrammarLexer.DIRECTIVE, "Directive");
            
            if (node.ChildCount != 1)
                throw new InvalidOperationException(string.Format("Expected directive node on line {0} to have one child but found {1}", node.Line, node.ChildCount));

            var directiveNode = node.GetChild(0);
            ValidateNodeType(directiveNode, ScriptGrammarLexer.PrefixedDirective, "Prefixed Directive");

            if (!directiveNode.Text.StartsWith("#"))
                throw new InvalidOperationException(string.Format("Expected directive node '{0}' on line {1} to begin with #", directiveNode.Text, directiveNode.Line));

            var text = directiveNode.Text.Substring(1);
            var arguments = directiveNode.GetChildren().Select(BuildArgument).ToList();

            return new DirectiveDefinition(text, arguments);
        }

        private static OperationDefinition BuildCommandWithHandler(ITree node)
        {
            var allChildren = node.GetChildren();
            var actions = allChildren.Where(i => i.Type == ScriptGrammarParser.Identifier).Select(BuildActionDefinition).ToList();

            if(actions.Count < 1 || actions.Count > 2)
                throw new InvalidOperationException(string.Format("Expected command line {0} to have 1 or 2 actions but found {1}", node.Line, actions.Count));

            var requestAction = actions.First();
            var responseAction = actions.Count == 2 ? actions.Last() : ActionDefinition.DoNothing;

            var impersonationInstructions = allChildren.Where(i => i.Type == ScriptGrammarParser.ImpersonationKeyword).Select(BuildImpersonationDefinition).ToList();

            if(impersonationInstructions.Count > 1)
                throw new InvalidOperationException(string.Format("Expected there to to zero or one impersonation instruction but found {0}.", impersonationInstructions.Count));

            ImpersonationDefinition impersonation = impersonationInstructions.Any() ? impersonationInstructions.Single() : null;

            return new OperationDefinition(requestAction, responseAction, impersonation);
        }

        private static string BuildComment(ITree node)
        {
            ValidateNodeType(node, ScriptGrammarLexer.COMMENT, "Comment");

            if (node.ChildCount != 1)
                throw new InvalidOperationException(string.Format("Expected comment node on line {0} to have one child but found {1}", node.Line, node.ChildCount));

            if (!node.GetChild(0).Text.StartsWith("//"))
                throw new InvalidOperationException(string.Format("Expected comment on line {0} to start with //", node.Line));

            return node.GetChild(0).Text.Substring("//".Length);
        }

        private static ActionDefinition BuildActionDefinition(ITree node)
        {
            ValidateNodeType(node, ScriptGrammarLexer.Identifier, "Identifier");

            return new ActionDefinition(node.Text, node.GetChildren().Select(BuildArgument).ToList());
        }

        private static ImpersonationDefinition BuildImpersonationDefinition(ITree node)
        {
            ValidateNodeType(node, ScriptGrammarLexer.ImpersonationKeyword, "Impersonation keyword");

            var childNodes = node.GetChildren();
            if(childNodes.Count != 2)
                throw new InvalidOperationException(string.Format("Expected impersonation instruction on line {0} to have two arguments but found {1}. Must specify username and password. Username can optionally include domain (e.g. 'domain\\username')", node.Line, childNodes.Count));

            var username = BuildArgument(childNodes[0]);
            var password = BuildArgument(childNodes[1]);

            return new ImpersonationDefinition(username, password);
        }

        private static string BuildArgument(ITree node)
        {
            ValidateNodeType(node, ScriptGrammarLexer.QuotedString, "String");

            return node.Text.Substring(1, node.Text.Length - 2).Replace("\"\"", "\"");
        }

        private static void ValidateNodeType(ITree node, int expectedType, string expectedTypeName)
        {
            if (node.Type != expectedType)
                throw new InvalidOperationException(string.Format("Expected '{0}' (at line {1}, column {2}) to be type {3} but it was {4}", node.Text, node.Line, node.CharPositionInLine, expectedTypeName, node.Type));
        }
    }
}
